package crmdna.client.member;

import static crmdna.common.OfyService.ofy;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.appengine.api.NamespaceManager;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.VoidWork;
import com.googlecode.objectify.Work;
import com.googlecode.objectify.cmd.Query;

import crmdna.client.contactdetail.ContactDetail.ContactDetailProp;
import crmdna.client.member.InteractionCore.InteractionProp;
import crmdna.client.member.InteractionCore.InteractionType;
import crmdna.client.practice.Practice;
import crmdna.client.practice.Practice.PracticeProp;
import crmdna.client.program.Program;
import crmdna.client.program.Program.ProgramProp;
import crmdna.client.programtype.ProgramType;
import crmdna.client.sequence.Sequence;
import crmdna.client.sequence.Sequence.SequenceType;
import crmdna.common.APIException;
import crmdna.common.APIResponse.Status;
import crmdna.common.Utils;

public class MemberCore {
	public static class MemberProp implements Comparable<MemberProp> {
		public long memberId;
		public ContactDetailProp contact;
		public String name;
		
		//public Set<String> qsTags = new HashSet<>();
		public Set<Long> groupIds = new HashSet<Long>();
		public Set<Long> programIds = new HashSet<Long>();
		public Set<Long> programTypeIds = new HashSet<Long>();
		public Set<Long> practiceIds = new HashSet<Long>();
		
		public Set<Long> interactionIds = new HashSet<Long>();
		
		public List<UnverifiedProgramProp> unverifiedProgramProps;

		@Override
		public int compareTo(MemberProp o) {
			return this.name.compareTo(o.name);
		}				
	}
				
	static MemberProp create(String namespace, long groupId, ContactDetailProp contact) {
														
		ensureEmailOrPhoneNumberValid(contact);
			
		//if email is specified - it should be unique	
		if (null != contact.email) {
			contact.email = contact.email.toLowerCase();
			ensureEmailNotPresentInDB(namespace, contact.email);	
			safeAddToMemcache(namespace, contact.email);
		}
						
		MemberEntity memberEntity = new MemberEntity();
		memberEntity.memberId = Sequence.getNext(namespace, SequenceType.MEMBER);		
		memberEntity.groupIds.add(groupId);

		memberEntity = populateContactDetails(memberEntity, contact);
		memberEntity = populateDependantIndexFieldsWithoutDBQuery(namespace, memberEntity);
		
		ofy(namespace).save().entity(memberEntity).now();
				
		return memberEntity.toProp();				
	}
	
	static void safeAddToMemcache(String namespace, String email) {
		//automatically add key to memcache, if already found throw exception
		
		String key = getUniqueKey(namespace, email.toLowerCase());
		NamespaceManager.set(namespace);
		long val = MemcacheServiceFactory.getMemcacheService().increment(key, 1l, 0l);
		//note: call to increment is atomic
		if (val != 1)
			throw new APIException(Status.ERROR_RESOURCE_ALREADY_EXISTS, 
					"There is already a member with email [" + email + "]");		
	}
	
	private static String getUniqueKey(String namespace, String name) {
		return namespace + "_" + SequenceType.MEMBER + "_" + name;
 	}
	
	static void ensureEmailNotPresentInDB(String namespace, String email) {
		
		email = email.toLowerCase();		
		List<Key<MemberEntity>> keys = ofy(namespace).load().type(MemberEntity.class)
				.filter("email", email).keys().list();
		
		if (keys.size() != 0)
			throw new APIException(Status.ERROR_RESOURCE_ALREADY_EXISTS, 
					"There is already a member with email [" + email + "]");	
	}
	
	static MemberProp updateContactDetails(final String namespace, final long memberId, 
			final ContactDetailProp contact) {						
				
		MemberEntity memberEntity = safeGet(namespace, memberId);
				
		//if email is different it should not exist in a different entity
		if ((memberEntity.email != null) && (contact.email != null) && 
				(! memberEntity.email.toLowerCase().equals(contact.email.toLowerCase()))) {
			ensureEmailNotPresentInDB(namespace, contact.email);	
			safeAddToMemcache(namespace, contact.email);
		}
				
		MemberProp memberProp = ofy(namespace).transact(new Work<MemberProp>() {

			@Override
			public MemberProp run() {
				MemberEntity memberEntity = safeGet(namespace, memberId);
				memberEntity = populateContactDetails(memberEntity, contact);
				memberEntity = populateDependantIndexFields(namespace, memberEntity);
				ofy(namespace).save().entity(memberEntity).now();
				
				return memberEntity.toProp();				
			}
		});
		
		return memberProp;					
	}
	
	static MemberProp addOrDeleteGroup(final String namespace, final long memberId, 
			final long groupId, final boolean add) {		
		
		MemberProp memberProp = ofy(namespace).transact(new Work<MemberProp>() {
			@Override
			public MemberProp run() {
				MemberEntity memberEntity = safeGet(namespace, memberId);		
				
				boolean change;
				if (add) {
					change = memberEntity.groupIds.add(groupId);
				} else {
					change = memberEntity.groupIds.remove(groupId);
					if (memberEntity.groupIds.size() == 0)
						throw new APIException(Status.ERROR_PRECONDITION_FAILED, 
								"A member should be assigned to atleast 1 group");
				}
				
				if (change)
					ofy(namespace).save().entity(memberEntity).now();
				
				return memberEntity.toProp();
			}
		});
		
		return memberProp;		
	}
	
	static MemberEntity populateContactDetails(MemberEntity memberEntity, ContactDetailProp contact) {
		if (contact.email != null) {
			Utils.ensureValidEmail(contact.email);
			memberEntity.email = contact.email.toLowerCase();
		}
								
		if (contact.firstName != null)
			memberEntity.firstName = contact.firstName;			
		
		if (contact.lastName != null)
			memberEntity.lastName = contact.lastName;
		
		if (contact.homeAddress != null)
			memberEntity.homeAddress = contact.homeAddress;
		
		if (contact.officeAddress != null)
			memberEntity.officeAddress = contact.officeAddress;
		
		if (contact.homePhone != null) {
			Utils.ensureValidPhoneNumber(contact.homePhone);
			memberEntity.homePhone = contact.homePhone;
		}
		
		if (contact.officePhone != null) {
			Utils.ensureValidPhoneNumber(contact.officePhone);
			memberEntity.officePhone = contact.officePhone;
		}
		
		if (contact.mobilePhone != null) {
			Utils.ensureValidPhoneNumber(contact.mobilePhone);
			memberEntity.mobilePhone = contact.mobilePhone;
		}		
						
		return memberEntity;
	}		
	
	static MemberEntity populateDependantIndexFields(String client, final MemberEntity memberEntity) {
		populateDependantIndexFieldsWithoutDBQuery(client, memberEntity);
		
		//fields that involve database queries
		HashSet<Long> programTypeIds = new HashSet<>();		
		
		Map<Long, ProgramProp> programProps = Program.get(client, memberEntity.programIds);
		
		for (Long programId : programProps.keySet()) {
			ProgramProp programProp = programProps.get(programId);
			
			if (null != programProp) {
				programTypeIds.add(programProp.programTypeProp.programTypeId);								
			}
		}
		
		//unverified programs
		List<UnverifiedProgramProp> unverified = Member.getUnverifiedPrograms(client, 
				memberEntity.memberId);		
		for (UnverifiedProgramProp up : unverified) {
			programTypeIds.add(up.programTypeId);
		}
						
		memberEntity.programTypeIds = programTypeIds;
		memberEntity.practiceIds = ProgramType.getPracticeIds(client, programTypeIds);
						
		return memberEntity;
	}
	
	static MemberEntity populateDependantIndexFieldsWithoutDBQuery(String client, final MemberEntity memberEntity) {
		//nameFirstChar
		if (memberEntity.firstName != null) {
			memberEntity.name = memberEntity.firstName.toLowerCase();			
		}
		else if (memberEntity.email != null)
			memberEntity.name = memberEntity.email.toLowerCase();
		else if (memberEntity.mobilePhone != null)
			memberEntity.name = memberEntity.mobilePhone.toLowerCase();
		else if (memberEntity.homePhone != null)
			memberEntity.name = memberEntity.homePhone.toLowerCase();
		else if (memberEntity.officePhone != null)
			memberEntity.name = memberEntity.officePhone.toLowerCase();
		else {
			//should never come here as either email or a valid phone number should be specified
			throw new APIException(Status.ERROR_RESOURCE_INCORRECTLY_SPECIFIED, 
					"Either email or a valid phone number should be specified when adding or updating member");
		}		
		memberEntity.nameFirstChar = memberEntity.name.substring(0, 1);
		
		memberEntity.qsTags = Utils.getQSTags(memberEntity.email, memberEntity.firstName,
				memberEntity.lastName, memberEntity.homePhone, memberEntity.officePhone,
				memberEntity.mobilePhone);
		
		//phone numbers
		HashSet<String> phoneNumbers = new HashSet<>();
		if (memberEntity.mobilePhone != null)
			phoneNumbers.add(memberEntity.mobilePhone);
		if (memberEntity.officePhone != null)
			phoneNumbers.add(memberEntity.officePhone);
		if (memberEntity.homePhone != null)
			phoneNumbers.add(memberEntity.homePhone);
		
		memberEntity.phoneNumbers = phoneNumbers;
		
		return memberEntity;
	}
	
	static MemberEntity safeGet(String namespace, long memberId) {		
				
		MemberEntity entity = ofy(namespace).load().type(MemberEntity.class).id(memberId).get();
		
		if (null == entity)
			throw new APIException(Status.ERROR_RESOURCE_DOES_NOT_EXIST, 
					"There is no member with id [" + memberId + "]");
		
		return entity;
	}
	
	static InteractionProp addInteraction(final String namespace, final long memberId, 
			String interaction, InteractionType interactionType, Date timestamp, long userId) {										
		
		//ensure memberId is valid
		safeGet(namespace, memberId);
		
		if (null == timestamp)
			timestamp = new Date();
		
		final InteractionProp interactionProp = InteractionCore.create(namespace, interaction, 
				interactionType, timestamp, userId);
		
		//update memberEntity in a transaction
		ofy(namespace).transact(new VoidWork() {
			
			@Override
			public void vrun() {
				MemberEntity memberEntity = safeGet(namespace, memberId);				
				memberEntity.interactionIds.add(interactionProp.interactionId);
				ofy(namespace).save().entity(memberEntity).now();
			}
		});
											
		return interactionProp;
	}		
	
	static MemberProp deleteInteraction(final String namespace, final long memberId,
			final long interactionId) {												
		
		//ensure memberId is valid
		safeGet(namespace, memberId);
		
		InteractionCore.delete(namespace, interactionId);
						
		//update memberEntity in a transaction
		MemberProp memberProp = ofy(namespace).transact(new Work<MemberProp>() {
			
			@Override
			public MemberProp run() {
				MemberEntity memberEntity = safeGet(namespace, memberId);				
				memberEntity.interactionIds.remove(interactionId);
				ofy(namespace).save().entity(memberEntity).now();
				return memberEntity.toProp();
			}
		});
											
		return memberProp;
	}		
	
	static List<MemberProp> query(String namespace, String searchString, Set<Long> groupIds, 
			Set<Long> practiceIds, Integer maxResultSize) {													
		
		Query<MemberEntity> q = ofy(namespace).load().type(MemberEntity.class);
		
		if ((null != groupIds) && (groupIds.size() != 0)) 			
			q = q.filter("groupIds in", groupIds);
		
		if ((null != practiceIds) && (practiceIds.size() != 0)) 			
			q = q.filter("practiceIds in", practiceIds);

		if (null != searchString) {
			if (searchString.length() < 3)
				Utils.throwIncorrectSpecException("Search string when specified should be atleast 3 chars");
			searchString = searchString.toLowerCase();
			Set<String> searchTags = Utils.getQSTags(searchString);
			for (String tag : searchTags) {
				q = q.filter("qsTags", tag);
			}
		}
		
		List<MemberEntity> entities = q.list();
		
		List<MemberProp> props = new ArrayList<>();
		for (MemberEntity entity : entities) 
			props.add(entity.toProp());
		
		Collections.sort(props);
		
		if ((null != maxResultSize) && (maxResultSize > 0))
			if (props.size() > maxResultSize)
				props = props.subList(0, maxResultSize);
		
		return props;						
	}	
	
	static List<MemberProp> getAllForGroup(String namespace, long groupId) {
								
		Query<MemberEntity> q = ofy(namespace).load().type(MemberEntity.class)
				.filter("groupIds", groupId);
		
		List<MemberEntity> entities = q.list();
		List<MemberProp> props = new ArrayList<>();
		for (MemberEntity entity : entities) 
			props.add(entity.toProp());
		
		Collections.sort(props);
		return props;
	}
	
	static void ensureEmailOrPhoneNumberValid(ContactDetailProp contact) {
		
		if (contact.email != null)
			Utils.ensureValidEmail(contact.email);
		
		if (null != contact.homePhone)
			Utils.ensureValidPhoneNumber(contact.homePhone);
		
		if (null != contact.mobilePhone)
			Utils.ensureValidPhoneNumber(contact.mobilePhone);
		
		if (null != contact.officePhone)
			Utils.ensureValidPhoneNumber(contact.officePhone);
		
		//email or one of the phone numbers should be specified
		if ((contact.email == null) && (contact.mobilePhone == null) && 
				(contact.homePhone == null) && (contact.officePhone == null)) {
			throw new APIException(Status.ERROR_RESOURCE_INCORRECTLY_SPECIFIED, 
					"Either email or one of the phone numbers should be specified");
		}
    }		
}